package com.alone.openai.api.core;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StopWatch;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author Alone
 */
public interface ApiDefinition<IN, OUT> extends Function<IN, OUT> {

    /**
     * 执行请求
     *
     * @param in 入参
     * @return OUT
     */
    default OUT exec(IN in) {
        return apply(in);
    }

    /**
     * 空入参
     *
     * @return OUT
     */
    default OUT exec() {
        return apply(null);
    }


    /**
     * 返回当前备份的构建器以创建其他副本
     *
     * @return ApiDefinition.BuilderEditor<IN, OUT>
     */
    ApiDefinition.BuilderEditor<IN, OUT> customize();

    /**
     * Api构建器
     *
     * @param reqClass  请求
     * @param respClass 响应
     * @param <IN>      reqClass
     * @param <OUT>     respClass
     * @return Api构建器
     */
    static <IN, OUT> Builder<IN, OUT> builder(Class<? extends IN> reqClass, Class<? extends OUT> respClass) {
        return new Builder<>(reqClass, respClass);
    }

    /**
     * Api构建器, 没有动态入参，写死的请求
     *
     * @param respClass 响应
     * @param <OUT>     respClass
     * @return Api构建器
     */
    static <OUT> Builder<Void, OUT> builder(Class<? extends OUT> respClass) {
        return new Builder<>(Void.class, respClass);
    }

    /**
     * Api构建器, 没有动态入参，写死的请求
     *
     * @return Api构建器
     */
    static Builder<Void, String> builder() {
        return new Builder<>(Void.class, String.class);
    }

    class Builder<IN, OUT> {

        public static final RestTemplate DEFAULT_CLIENT;
        private static final Logger LOG = LoggerFactory.getLogger(Builder.class);

        // init client
        static {
            DEFAULT_CLIENT = new RestTemplate();
        }

        // base data
        private final Class<? extends IN> reqClass;
        private final Class<? extends OUT> respClass;
        private String url;
        private HttpMethod method = HttpMethod.GET;
        private MultiValueMap<String, Function<IN, String>> header;
        private Map<String, Function<IN, Object>> param;
        private Map<String, Function<IN, Object>> path;
        private Set<String> pathValPlaceholder;
        private Function<IN, ?> body;
        private RestOperations client = DEFAULT_CLIENT;

        // feature

        /**
         * 以application/x-www-form-urlencoded方式提交body
         * 默认为application/json格式
         */
        private boolean formData = false;

        /**
         * 请求信息打印
         */
        private boolean log = false;

        /**
         * 自动从param中取path需要到参数
         * 不需要通过方法 {@link #path(String, Function)} {@link #path(String, Object)} 设置值，不过优先级是这两个方法高
         */
        private boolean autoMapParamToPath = true;

        public Builder(Class<? extends IN> reqClass, Class<? extends OUT> respClass) {
            this.reqClass = reqClass;
            this.respClass = respClass;
        }

        /**
         * 设置请求客户端
         */
        public Builder<IN, OUT> client(RestOperations client) {
            this.client = client;
            return this;
        }

        /**
         * 将 body 以 form data 表单提交
         */
        public Builder<IN, OUT> enableFormData() {
            this.formData = true;
            return this;
        }

        /**
         * 开启日志
         */
        public Builder<IN, OUT> enableLog() {
            this.log = true;
            return this;
        }


        public Builder<IN, OUT> disableAutoMapParamToPath() {
            this.autoMapParamToPath = false;
            return this;
        }

        /**
         * 将入参完全映射到param
         */
        public Builder<IN, OUT> mapToParam() {
            PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(reqClass);
            if (this.param == null) {
                this.param = new HashMap<>(propertyDescriptors.length);
            }
            mapTo(param, propertyDescriptors);
            return this;
        }

        /**
         * 将入参完全映射到path
         */
        public Builder<IN, OUT> mapToPath() {
            PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(reqClass);
            if (this.path == null) {
                this.path = new HashMap<>(propertyDescriptors.length);
            }
            mapTo(path, propertyDescriptors);
            return this;
        }

        /**
         * 将入参完全映射到body
         */
        public Builder<IN, OUT> mapToBody() {
            this.body = in -> in;
            return this;
        }

        private void mapTo(Map<String, Function<IN, Object>> container, PropertyDescriptor[] data) {
            for (PropertyDescriptor propertyDescriptor : data) {
                // 忽略getClass()方法
                if ("class".equals(propertyDescriptor.getName())) {
                    continue;
                }
                container.put(propertyDescriptor.getName(), in -> {
                    if (Objects.isNull(in)) {
                        return null;
                    }
                    try {
                        Method readMethod = propertyDescriptor.getReadMethod();
                        readMethod.setAccessible(true);
                        return readMethod.invoke(in);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        throw new RuntimeException(e);
                    }
                });
            }
        }

        /**
         * 设置请求地址
         */
        public Builder<IN, OUT> url(String url) {
            this.url = url;
            return this;
        }

        /**
         * 设置请求方式
         */
        public Builder<IN, OUT> method(HttpMethod method) {
            this.method = method;
            return this;
        }

        /**
         * 设置请求头
         */
        public Builder<IN, OUT> header(String key, String value) {
            if (this.header == null) {
                this.header = new LinkedMultiValueMap<>();
            }
            this.header.add(key, in -> value);
            return this;
        }

        /**
         * 设置请求头
         */
        public Builder<IN, OUT> header(String key, Function<IN, String> valueFunc) {
            if (this.header == null) {
                this.header = new LinkedMultiValueMap<>();
            }
            this.header.add(key, valueFunc);
            return this;
        }

        /**
         * 设置请求参数
         */
        public Builder<IN, OUT> param(String key, Object value) {
            if (this.param == null) {
                this.param = new HashMap<>();
            }
            this.param.put(key, in -> value);
            return this;
        }

        /**
         * 设置请求参数
         */
        public Builder<IN, OUT> param(String key, Function<IN, Object> valueFunc) {
            if (this.param == null) {
                this.param = new HashMap<>();
            }
            this.param.put(key, valueFunc);
            return this;
        }

        /**
         * 设置请求路径值
         */
        public Builder<IN, OUT> path(String key, Object value) {
            if (this.path == null) {
                this.path = new HashMap<>();
            }
            this.path.put(key, in -> value);
            return this;
        }

        /**
         * 设置请求路径值
         */
        public Builder<IN, OUT> path(String key, Function<IN, Object> valueFunc) {
            if (this.path == null) {
                this.path = new HashMap<>();
            }
            this.path.put(key, valueFunc);
            return this;
        }

        /**
         * 设置请求体
         */
        public Builder<IN, OUT> body(Object body) {
            this.body = in -> body;
            return this;
        }

        /**
         * 设置请求体
         */
        public Builder<IN, OUT> body(Function<IN, Object> body) {
            this.body = body;
            return this;
        }

        /**
         * 设置请求体
         */
        public Builder<IN, OUT> body(Consumer<Map<String, Object>> body) {
            Map<String, Object> bodyMap = new HashMap<>();
            body.accept(bodyMap);
            this.body = in -> bodyMap;
            return this;
        }

        /**
         * 设置请求体
         */
        public Builder<IN, OUT> multiValBody(Consumer<MultiValueMap<String, Object>> body) {
            MultiValueMap<String, Object> bodyMap = new LinkedMultiValueMap<>();
            body.accept(bodyMap);
            this.body = in -> bodyMap;
            return this;
        }

        public ApiDefinition<IN, OUT> build() {
            check();
            handlePathValue();
            Builder<IN, OUT> ref = this;
            return new ApiDefinition<IN, OUT>() {
                private BuilderEditor<IN, OUT> builderClone = null;

                @Override
                public BuilderEditor<IN, OUT> customize() {
                    if (builderClone == null) {
                        builderClone = BuilderEditor.clone(ref);
                    }
                    return builderClone;
                }

                @Override
                public OUT apply(IN in) {
                    return execution(in);
                }
            };
        }

        private OUT execution(IN in) {
            MultiValueMap<String, String> mapHeader = apply(in, header);
            Object mapBody = Objects.nonNull(body) ? body.apply(in) : null;

            if (formData) {
                mapHeader = null == mapHeader ? new LinkedMultiValueMap<>(1) : mapHeader;
                mapHeader.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
                mapBody = bodyToMultiValueMap(mapBody);
            }

            HttpEntity<Object> entity;
            if (empty(mapHeader) && null == mapBody) {
                entity = null;
            } else {
                entity = new HttpEntity<>(mapBody, mapHeader);
            }
            Map<String, Object> mapParam = apply(in, param);
            Map<String, Object> mapPath = apply(in, path);

            StopWatch stopWatch = null;
            if (log) {
                stopWatch = new StopWatch();
                stopWatch.start();
            }

            OUT res = client.exchange(
                    splicingUriValues(url, mapParam, mapPath),
                    method, entity,
                    respClass).getBody();

            if (log) {
                assert stopWatch != null;
                stopWatch.stop();
                String result = null != res ? res.toString() : "";
                LOG.info("\n╔═════════════════请求接口═════════════════╗\n" +
                                "║请求地址：{}\n" +
                                "║请求方式：{}\n" +
                                "║请求头：{}\n" +
                                "║请求参数：{}\n" +
                                "║请求体：{}\n" +
                                "║返回数据：{}\n" +
                                "║请求耗时：{}\n" +
                                "╚═════════════════════════════════════════╝\n",
                        url,
                        method,
                        mapHeader,
                        mapParam,
                        mapBody,
                        result.length() >= 300 ? result.substring(0, 300) + " ..." : result,
                        stopWatch.getTotalTimeMillis() + "ms"
                );
            }

            return res;
        }

        private static final Pattern PATH_VAL_PATTERN = Pattern.compile("\\{(.*?)}");

        private void handlePathValue() {
            Matcher matcher = PATH_VAL_PATTERN.matcher(url);
            Set<String> set = new HashSet<>();
            while (matcher.find()) {
                set.add(matcher.group(1));
            }
            this.pathValPlaceholder = set;
        }

        private void check() {
            if (null == url || url.isEmpty()) {
                throw new IllegalArgumentException("url不能为空");
            }
            if (null == method) {
                throw new IllegalArgumentException("method不能为空");
            }
        }

        /**
         * adapt form data request
         * 将body转换成 MultiValueMap
         */
        @SuppressWarnings({"unchecked", "rawtypes"})
        private MultiValueMap<String, Object> bodyToMultiValueMap(Object body) {
            if (Objects.isNull(body)) {
                return null;
            }
            if (body instanceof MultiValueMap) {
                return (MultiValueMap<String, Object>) body;
            } else if (body instanceof Map) {
                Map<String, Object> bodyMap = (Map) body;
                MultiValueMap<String, Object> res = new LinkedMultiValueMap<>(bodyMap.size());
                bodyMap.forEach((k, v) -> {
                    if (v instanceof List) {
                        res.put(k, (List) v);
                    } else if (v.getClass().isArray()) {
                        res.put(k, Arrays.asList((Object[]) v));
                    } else if (v instanceof Collection) {
                        res.put(k, new ArrayList<>((Collection) v));
                    } else if (v instanceof Iterable) {
                        Iterable it = (Iterable) v;
                        for (Object o : it) {
                            res.add(k, o);
                        }
                    } else {
                        res.add(k, v);
                    }
                });
                return res;
            } else {
                PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(body.getClass());
                MultiValueMap<String, Object> res = new LinkedMultiValueMap<>(propertyDescriptors.length);
                for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                    // 忽略getClass()方法
                    if ("class".equals(propertyDescriptor.getName())) {
                        continue;
                    }
                    Method readMethod = propertyDescriptor.getReadMethod();
                    readMethod.setAccessible(true);
                    try {
                        res.add(propertyDescriptor.getName(), readMethod.invoke(body));
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        throw new RuntimeException(e);
                    }
                }
                return res;
            }
        }

        /**
         * 填充 path 和 param
         */
        private String splicingUriValues(String url, Map<String, Object> requestParams, Map<String, Object> requestPaths) {
            boolean emptyParams = empty(requestParams);
            boolean emptyPaths = empty(requestPaths);
            if (emptyParams && emptyPaths) {
                if (empty(pathValPlaceholder)) {
                    return url;
                } else {
                    throw new IllegalStateException("path value : " + pathValPlaceholder + " required");
                }
            }

            StringBuilder sb = new StringBuilder();
            String res = url;

            // handle path
            if (!emptyPaths) {
                for (String key : requestPaths.keySet()) {
                    if (pathValPlaceholder.contains(key)) {
                        Object val = requestPaths.get(key);
                        if (Objects.isNull(val) && !autoMapParamToPath) {
                            throw new IllegalStateException("path value : {" + key + "} required");
                        } else {
                            res = res.replace("{" + key + "}", toParamStr(val));
                            // consume placeholder
                            pathValPlaceholder.remove(key);
                        }
                    }
                }
            }

            sb.append("?");

            // handle param
            if (!emptyParams) {
                for (String key : requestParams.keySet()) {
                    // handle surplus path if function open
                    if (autoMapParamToPath) {
                        if (pathValPlaceholder.contains(key)) {
                            Object val = requestParams.get(key);
                            if (Objects.isNull(val)) {
                                throw new IllegalArgumentException("path value : {" + key + "} required");
                            } else {
                                res = res.replace("{" + key + "}", toParamStr(val));
                                // consume placeholder
                                pathValPlaceholder.remove(key);
                            }
                            continue;
                        }
                    }
                    // do handle param
                    Object val = requestParams.get(key);
                    if (Objects.isNull(val)) {
                        continue;
                    }
                    sb.append(key).append("=").append(toParamStr(val)).append("&");
                }
            }

            // check path has fill complete
            if (!pathValPlaceholder.isEmpty()) {
                throw new IllegalStateException("path value : " + pathValPlaceholder + " required");
            }

            int andIndex = sb.lastIndexOf("&");
            if (andIndex != -1) {
                sb.deleteCharAt(andIndex);
            }
            return res + sb;
        }

        /**
         * 如果是迭代器或者数组 就用逗号拼接
         * 其他就toString
         */
        @SuppressWarnings("rawtypes")
        private String toParamStr(Object val) {
            String value;
            if (val instanceof Iterable) {
                Iterable it = (Iterable) val;
                StringBuilder valSb = new StringBuilder();
                for (Object o : it) {
                    valSb.append(o.toString()).append(",");
                }
                value = valSb.deleteCharAt(valSb.lastIndexOf(",")).toString();
            } else if (val.getClass().isArray()) {
                value = Arrays.stream((Object[]) val)
                        .map(Object::toString)
                        .collect(Collectors.joining(","));
            } else {
                value = val.toString();
            }
            return value;
        }

        private <T> MultiValueMap<String, T> apply(IN in, MultiValueMap<String, Function<IN, T>> target) {
            if (empty(target)) {
                return null;
            }
            MultiValueMap<String, T> res = new LinkedMultiValueMap<>(target.size());
            target.forEach((key, value) -> {
                List<T> values = value.stream()
                        .map(func -> func.apply(in))
                        .collect(Collectors.toList());
                res.addAll(key, values);
            });
            return res;
        }

        private <T> Map<String, T> apply(IN in, Map<String, Function<IN, T>> target) {
            if (empty(target)) {
                return null;
            }
            Map<String, T> res = new HashMap<>(target.size());
            target.forEach((key, value) -> res.put(key, value.apply(in)));
            return res;
        }

        private boolean empty(Map<String, ?> target) {
            return null == target || target.isEmpty();
        }

        private boolean empty(Set<?> target) {
            return null == target || target.isEmpty();
        }

    }

    class BuilderEditor<IN, OUT> extends Builder<IN, OUT> {

        private static <IN, OUT> BuilderEditor<IN, OUT> clone(Builder<IN, OUT> ref) {
            return new BuilderEditor<>(ref);
        }

        private BuilderEditor(Builder<IN, OUT> ref) {
            super(ref.reqClass, ref.respClass);
            // copy column
            super.url = ref.url;
            super.method = ref.method;
            super.path = ref.path != null ? new HashMap<>(ref.path) : null;
            super.param = ref.param != null ? new HashMap<>(ref.param) : null;
            super.header = ref.header != null ? new LinkedMultiValueMap<>(ref.header) : null;
            super.body = ref.body;
            super.pathValPlaceholder = ref.pathValPlaceholder != null ? new HashSet<>(ref.pathValPlaceholder) : null;
            super.client = ref.client != null ? ref.client : DEFAULT_CLIENT;
            super.formData = ref.formData;
            super.log = ref.log;
            super.autoMapParamToPath = ref.autoMapParamToPath;
        }

        public BuilderEditor<IN, OUT> disableLog() {
            super.log = false;
            return this;
        }

        public BuilderEditor<IN, OUT> disableFormData() {
            super.formData = false;
            return this;
        }

        public BuilderEditor<IN, OUT> enableAutoMapParamToPath() {
            super.autoMapParamToPath = true;
            return this;
        }

        public BuilderEditor<IN, OUT> removeHeader(String key) {
            super.header.remove(key);
            return this;
        }

        public BuilderEditor<IN, OUT> removeParam(String key) {
            super.param.remove(key);
            return this;
        }

        public BuilderEditor<IN, OUT> removePath(String key) {
            super.path.remove(key);
            return this;
        }

    }
}


